# midinote.py

"""
This module is an integeral part of the program
MMA - Musical Midi Accompaniment.

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

Bob van der Poel <bob@mellowood.ca>

This module does all the midinote stuff.

"""


import MMA.notelen
import MMA.midiC

import gbl
from   MMA.common import *



def parse(name, ln):
    """ Called from parser for a <Track MidiNote> This figures the right routine."""

    if not len(ln):
        error("MidiNote: Needs arguments")
    
    trk=gbl.tnames[name]
    
    # parse out the cmd=value options pairs

    ln, opts = opt2pair(ln, toupper=0)

    for o,v in opts:
        o=o.upper()
        v=v.upper()
        if o == 'TRANSPOSE':
            if v in ('0', 'OFF'):
                trk.transpose = 0
            elif v in ('1', 'ON'):
                trk.transpose = gbl.transpose
            else:
                error("MIDINote: TRANSPOSE expecting ON or OFF, not %s." % v)

        elif o == 'OFFSETS':
            if v == 'BEATS':
                trk.useticks = 0
            elif v == 'TICKS':
                trk.useticks = 1
            else:
                error("MIDINote: OFFSETS expecting BEATS or TICKS, not %s." % v)

        elif o == 'DURATION':
            if v == 'NOTES':
                trk.tickdur = 0
            elif v == 'TICKS':
                trk.tickdur = 1
            else:
                error("MIDINote: DURATION expecting NOTES or TICKS, not %s." % v)

        elif o == 'ARTICULATE':
            if v in ('1,' 'ON'):
                trk.articulate=1
            elif v in ('0', 'OFF'):
                trk.articulate=0
            else:
                error("MIDINote: ARTICULATE expecting ON or OFF, not %s." % v)

        elif o == 'OCTAVE':
            trk.oadjust = stoi(v)
            if trk.oadjust < -4 or trk.oadjust > 4:
                error("MIDINote: Octave adjustment must be -4..4, not '%s'." % trk.oadjust)

        elif o == 'VOLUME':
            trk.vadjust = stof(v)/100
            if trk.vadjust <= 0:
                error("MIDINote: Volume %% adjustment must be > 0, not '%s'." % trk.vadjust)

        elif o == 'ADJUST':
            trk.tadjust = stoi(v)

        else:
            error("MIDINote: unknown option pair %s=%s." % (o,v))
    
    # end of option pairs.

    if ln:  # process rest of the stuff ... real midi events.

        trk.setForceOut()

        a=ln[0].upper()

        if a[0] in (".0123456789"):
            insertNote(trk, ln)

        elif a == "NOTE":
            insertNote(trk, ln[1:])

        elif a  == "PB":
            insertPB(trk, ln[1:])

        elif a == "PBR":
            insertPBrange(trk, ln[1:])

        elif a == "CTRL":
            insertControl(trk, ln[1:])

        elif a == "CHAT":
            insertChTouch(trk, ln[1:])

        elif a == "CHATR":
            insertChTouchRange(trk, ln[1:])

        else:
            error("MidiNote: Unknown command '%s'." % a)

    if gbl.debug:
        if opts:
            print "MIDINOTE: %s" % mopts(trk)

def mopts(trk):
    """ Return options string to macro and setoption-debug. """

    if trk.useticks:
        a1='Ticks'
    else:
        a1='Beats'

    if trk.tickdur:
        a2='Ticks';
    else:
        a2='Notes'

    if trk.transpose:
        a3='On'
    else:
        a3='Off'

    if trk.articulate:
        a4='On'
    else:
        a4='Off'

    return "Offsets=%s Duration=%s Transpose=%s Articulate=%s Adjust=%s Volume=%s Octave=%s" \
        % (a1, a2, a3, a4, trk.tadjust, trk.vadjust * 100, trk.oadjust)


def getoffset(trk, v):
    """ Convert a string (value) to an offset. Convert to beats if nesc."""

    offset = stof(v)
    if trk.useticks:
        if offset != int(offset):
            error("MidiNote: Offset set to ticks, float '%s' given. Integer must be used." % offset)
        offset += trk.tadjust

    else:
        if offset < 1:
            warning("MidiNote: Offset %s generates notes before start of current bar." % offset)

        if offset >= gbl.QperBar + 1:
            warning("MidiNote: Offset %s is past end of current bar." % offset)

        offset = (offset-1) * gbl.BperQ

    return  int(offset)  # conversion to int is nescessary


def note2val(trk, acctable, orig):
    """ Convert a note name to a value. In this case the OCTAVE setting
        is used!
    """

    t = list(orig)
    n = t.pop(0)
    try:
        val = {'c':0, 'd':2, 'e':4, 'f':5, 'g':7, 'a':9, 'b':11 }[n]
    except:
        error("MidiNote: Expecting valid note name, not '%s'." % orig)

    val += trk.octave[gbl.seqCount]  # add in current octave

    # Modify the note with either the keysignature or given accidental.


    
    if t:   # override modifier if #,& or n
        if t[0] == '#':
            acctable[n] = 1
            t.pop(0)
        elif t[0] == '&':
            acctable[n] = -1
            t.pop(0)
        elif t[0] == 'n':
            acctable[n] = 0
            t.pop(0)

    val += acctable[n]

    # adjust for octave - or +

    while t and (t[0] == '-' or t[0] == '+'):
        if t[0]=='+':
            val+=12
        else:
            val-=12
        t.pop(0)

    if t:  #anything left? Error.
        error("MidiNote: Unknown note specifier '%s' in '%s'." % (''.join(t), orig))
    
    return val

def insertNote(trk, ln):
    """ Insert specified (raw) MIDI notes into track. """

    if len (ln) != 4:
        error("Use: %s MidiNote: <offset> <note> <velocity> <duration>" % trk.name)
    
    acctable = MMA.keysig.keySig.accList   # keysig modifier, use for chord

    offset=getoffset(trk, ln[0])

    # Set a flag if this is a drum track.

    if trk.vtype == 'DRUM':
        isdrum = 1
    elif trk.vtype in ('MELODY', 'SOLO') and trk.drumType:
        isdrum = 1
    else:
        isdrum = 0

    notes = []
    for n in ln[1].split(','):
        if n[0] in '0123456789':
            n = stoi(n)
        else:
            if isdrum:
                if n == '*':
                    if trk.vtype in ('MELODY', 'SOLO'):
                        n = trk.drumTone
                    else:
                        n = trk.toneList[gbl.seqCount]
                else:
                    n = MMA.midiC.drumToValue(n)
                    if n < 0:
                        error("MidiNote: unknown drum tone '%s' in %s." % (n, trk.name) )
            else:
                n = note2val(trk, acctable, n)

        if n < 0 or n >127:
            error("MidiNote: Notes must be in the range 0...127, not %s" % n)
            
        if trk.transpose and not isdrum:
            n += gbl.transpose
            while n < 0:
                n += 12
            while n > 127:
                n -= 12

        if trk.oadjust and not isdrum:
            n += (trk.oadjust * 12)
            while n < 0:
                n += 12
            while n > 127:
                n -= 12

        notes.append(n)

    velocity = stoi(ln[2])
 
    if velocity < 1 or velocity > 127:
        error("MidiNote: Note velocity must be in the range 1...127, not %s" % velocity)

    velocity *= trk.vadjust

    if velocity < 1:
        velocity = 1
    elif velocity > 127:
        velocity = 127
    
    velocity = int(velocity)   # trk.adjust can be a float

    if trk.tickdur:
        duration = stoi(ln[3])
    else:
        duration = MMA.notelen.getNoteLen(ln[3])
        if trk.articulate:
            duration = (duration * trk.artic[gbl.seqCount]) / 100
            if duration < 1:
                duration = 1

    channel = trk.channel
    track = gbl.mtrks[channel]

    for n in notes:
        onEvent  = chr(0x90 | channel-1) + chr(n) + chr(velocity)
        offEvent = onEvent[:-1] + chr(0)

        track.addToTrack(gbl.tickOffset + offset, onEvent)
        track.addToTrack(gbl.tickOffset + offset + duration, offEvent)

    if gbl.debug:
        print "MidiNote Note %s: inserted note %s at offset %s." % (trk.name, notes, offset)


def insertPB(trk, ln):
    """ Insert a pitch controller event. """

    if len(ln) != 2:
        error("MidiNote: PB expecting 2 arguments.")

    offset = getoffset(trk, ln[0])
    v = stoi(ln[1])

    if v<-8191 or v>8192:
        error("MidiNote: PB value must be -8191..+8192, not '%s'." % v)

    v+=8191  # convert to 0..16383, max 14 bit value

    channel = trk.channel
    track = gbl.mtrks[channel]
 
    track.addToTrack(gbl.tickOffset + offset, chr(0xe0 | channel-1) + chr(v%128) + chr(v/128))
   
    if gbl.debug:
        print "MidiNote PB %s: inserted bend %s at offset %s." % (trk.name, v-8191, offset)


def insertPBrange(trk, ln):
    """ Insert a range of PB events. """

    if len(ln) != 3:
        error("MidiNote: PBR expecting 3 arguments <count> <start,end> <v1,v2>.")

    count = stoi(ln[0])
    try:
        s1,s2 = ln[1].split(',')
    except:
        error("MidiNote PBR: event range must be 'v1,v2', not '%s'." % ln[1])
    s1 = getoffset(trk, s1)
    s2 = getoffset(trk, s2)
    tinc = (s2-s1)/float(count)

    try:
        v1,v2 = ln[2].split(',')
    except:
        error("MidiNote PBR: pitch blend range must be 'v1,v2', not '%s'." % ln[2])
    v1 = stoi(v1)
    v2 = stoi(v2)

    if v1<-8191 or v1>8192 or v2<-8191 or v2>8192:
        error("MidiNote: PBR values must be -8191..+8192, not '%s'." % ln[2])

    v1+=8191  # convert to 0..16383, max 14 bit value
    v2+=8191
    vinc = (v2-v1)/float(count)
    
    channel = trk.channel
    track = gbl.mtrks[channel]
    
    ev = chr(0xe0 | channel-1)
    offset = s1
    bend = v1
    for i in range(count+1):
        v = int(bend)
        track.addToTrack(gbl.tickOffset + int(offset), ev + chr(v%128) + chr(v/128))
        offset += tinc
        bend += vinc

    if gbl.debug:
        print "MidiNote PBR %s: inserted bends %s to %s at offsets %s to %s." % \
            (trk.name, v1-8191, v2-8191, s1, s2)

def insertControl(trk, ln):
    """ Insert a controller event. """

    if len(ln) != 3:
        error("MidiNote: Controller expecting 3 arguments.")

    offset = getoffset(trk, ln[0])

    v=MMA.midiC.ctrlToValue(ln[1])
    if v < 0:
        v=stoi(ln[1])
        if v < 0 or v > 0x7f:
            error("MidiNote: Controller values must be 0x00 to 0x7f, not '%s'." % ln[1])

    d=stoi(ln[2])
    if d < 0 or d > 0x7f:
        error("MidiNote: Control Datum value must be 0x00 to 0x7f, not '%s'." % ln[2])

    channel = trk.channel
    track = gbl.mtrks[channel]

    # bypass the addctl() defined in midi.py just to keep all the calls in this
    # module similar. We should have add**() command in midi.py for the above stuff
    # and redo this.???

    track.addToTrack(gbl.tickOffset + offset, chr(0xb0 | channel-1) + chr(v) + chr(d) )

    if gbl.debug:
        print "MidiNote Ctrl %s: inserted Controller %s value %s at offset %s." % \
            (trk.name, v, d, offset)


def insertChTouch(trk, ln):
    """ Insert a channel aftertouch) event. """

    if len(ln) != 2:
        error("MidiNote: ChAT expecting 2 arguments.")

    offset = getoffset(trk, ln[0])

    v = stoi(ln[1])

    if v<0 or v>127:
        error("MidiNote: ChAT value must be 0 .. 127, not '%s'." % v)

    channel = trk.channel
    track = gbl.mtrks[channel]
 
    track.addToTrack(gbl.tickOffset + offset, chr(0xd0 | channel-1) + chr(v) )
   
    if gbl.debug:
        print "MidiNote ChAT %s: inserted channel aftertouch %s at offset %s." % \
            (trk.name, v, offset)

def insertChTouchRange(trk, ln):
    """ Insert a range of channel aftertouch events. """

    if len(ln) != 3:
        error("MidiNote: ChATR expecting 3 arguments <count> <start,end> <v1,v2>.")

    count = stoi(ln[0])
    try:
        s1,s2 = ln[1].split(',')
    except:
        error("MidiNote ChATR: event range must be 'v1,v2', not '%s'." % ln[1])
    s1 = getoffset(trk, s1)
    s2 = getoffset(trk, s2)
    tinc = (s2-s1)/float(count)

    try:
        v1,v2 = ln[2].split(',')
    except:
        error("MidiNote ChATR: range must be 'v1,v2', not '%s'." % ln[2])
    v1 = stoi(v1)
    v2 = stoi(v2)

    if v1<01 or v1>127 or v2<0 or v2>127:
        error("MidiNote: ChATR values must be 0.. 127, not '%s'." % ln[2])

    vinc = (v2-v1)/float(count)
    
    channel = trk.channel
    track = gbl.mtrks[channel]
    
    ev = chr(0xd0 | channel-1)
    offset = s1
    bend = v1
    for i in range(count+1):
        v = int(bend)
        track.addToTrack(gbl.tickOffset + int(offset), ev + chr(v) )
        offset += tinc
        bend += vinc

    if gbl.debug:
        print "MidiNote ChATR %s: inserted events %s to %s at offsets %s to %s." % \
            (trk.name, v1, v2, s1, s2)
